Skip to content

Conversation

fernantho
Copy link
Contributor

@fernantho fernantho commented Oct 15, 2025

What type of PR is this?
Feature

Which issues(s) does this PR fix?
Partially #15598

What does this PR do? Why is it needed?
This PR develop a way to calculate the Generalized Indices of a given path within a SSZ Object. To do so, it follows Consensus Spec's Merkle proofs.

A conversion from PathElement to Generalized Index is necessary to work with fastssz proofs library.

The code implemented walks the path within the SSZInfo struct in a consensus layer spec way. We have to take into account that the "path" input is slightly different:

def get_generalized_index(typ: SSZType, *path: PyUnion[int, SSZVariableName]) -> GeneralizedIndex:
    """
    Converts a path (eg. `[7, "foo", 3]` for `x[7].foo[3]`, `[12, "bar", "__len__"]` for
    `len(x[12].bar)`) into the generalized index representing its position in the Merkle tree.
    """
// GetGeneralizedIndexFromPath calculates the generalized index for a given path.
// To calculate the generalized index, two inputs are needed:
// 1. The sszInfo of the root info, to be able to navigate the SSZ structure
// 2. The path to the field (e.g., "field_a.field_b[3].field_c")
// It walks the path step by step, updating the generalized index at each step.
func GetGeneralizedIndexFromPath(info *sszInfo, path []PathElement) (uint64, error) {

The pythonic version expects a Path input [FieldA,"FieldB",3] while the Go version expects field_a.field_b[3].

Other notes for review

func getChunkCount(info *sszInfo) (uint64, error) {
	case UintN, Byte, Boolean:
		return 1, nil
(...)
	case Vector:
		vectorInfo, err := info.VectorInfo()
		if err != nil {
			return 0, err
		}
		// For Vectors with basic element types, multiple elements can be packed into 32-byte chunks
		elementInfo, err := vectorInfo.Element()
		if err != nil {
			return 0, err
		}
		elemLength := itemLengthFromInfo(elementInfo)
		return (vectorInfo.Length()*uint64(elemLength) + 31) / bytesPerChunk, nil

a. In the case of "basic types", it is due to how the GetGeneralizedIndexFromPath is coded.
b. For the "Vector" type, I tried to create new custom types as per the following example:

message BlsPublicKey {
  bytes pubkey = 1 [ (ethereum.eth.ext.ssz_size) = "48" ];
}

message VectorContainer {
  repeated BlsPublicKey vector_field = 1 [ (ethereum.eth.ext.ssz_size) = "16" ];  // Vector[16] of uint64
}

But it fails to generate the Go wrappers.

  • Multidimensional arrays GI will be handled at a later iteration.

Acknowledgements

@fernantho fernantho marked this pull request as ready for review October 17, 2025 12:56
// Helpers for input processing

// processPathElement processes a path element string and returns an Element struct
func processPathElement(elementStr string) (Element, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these path-related processing can be done at path.go, so GetGeneralizedIndexFromPath won't process the path again. So Element in this file can be merged with PathElement (I'm not sure I've understood the PR description correctly but I think so)

One other concern for elementStr is that it's the result of ParsePath, which means the index information will probably be parsed. For example, if a path is like "bitlist_field[1]", then PathElement would look like:

&query.PathElement{
  Name: "bitlist_field",
  Index: &1 // pointer to uint64
}

}
return (bitlistInfo.Limit() + 255) / bitsPerChunk, nil // Bits are packed into 256-bit chunks
case Bitvector:
vectorInfo, err := info.BitvectorInfo()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
vectorInfo, err := info.BitvectorInfo()
bitvectorInfo, err := info.BitvectorInfo()

Just a nit!

return 0, err
}
elemLength := itemLengthFromInfo(elementInfo)
return (vectorInfo.Length()*uint64(elemLength) + 31) / bytesPerChunk, nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return (vectorInfo.Length()*uint64(elemLength) + 31) / bytesPerChunk, nil
return (vectorInfo.Length()*elemLength + 31) / bytesPerChunk, nil

Other nit, we don't need type cast here as it is already uint64. Same comment for List case above.

return 0, fmt.Errorf("len() is only supported for List and Bitlist types, got %s", fieldSsz.sszType)
}
currentInfo = &sszInfo{sszType: UintN, fixedSize: 8}
root = updateRoot(root, 1, 2, 1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
root = updateRoot(root, 1, 2, 1)
root = root*2 + 1

I think we can simply put the calculation here

default:
return 0, fmt.Errorf("indexing not supported for type %s", fieldSsz.sszType)
}
continue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
continue

We might remove continue here

multiplier = nextPowerOfTwo(innerChunkCount)
offset = *idx
}
root = updateRoot(root, 1, multiplier, offset)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use innerChunkCount instead of multiplier here, as List and Vector would probably share same logic of calculating root index?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants